BeanQueryLookupStrategy.java
package org.codefilarete.stalactite.spring.repository.query.bean;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.codefilarete.stalactite.engine.EntityPersister.ExecutableEntityQuery;
import org.codefilarete.stalactite.engine.EntityPersister.ExecutableProjectionQuery;
import org.codefilarete.stalactite.engine.ExecutableQuery;
import org.codefilarete.stalactite.spring.repository.StalactiteRepository;
import org.codefilarete.stalactite.spring.repository.query.BeanQuery;
import org.codefilarete.stalactite.spring.repository.query.StalactiteQueryMethod;
import org.codefilarete.stalactite.sql.Dialect;
import org.codefilarete.tool.Nullable;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.VisibleForTesting;
import org.codefilarete.tool.collection.Arrays;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.function.Predicates;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.RepositoryQuery;
/**
* {@link QueryLookupStrategy} that tries to detect a query declared via {@link BeanQuery} annotation.
*
* @author Guillaume Mary
*/
public class BeanQueryLookupStrategy<C> implements QueryLookupStrategy {
private final ListableBeanFactory beanFactory;
private final Dialect dialect;
/**
* Creates a new {@link BeanQueryLookupStrategy}.
*
*/
public BeanQueryLookupStrategy(ListableBeanFactory beanFactory,
Dialect dialect) {
this.beanFactory = beanFactory;
this.dialect = dialect;
}
@Override
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) {
BeanQueryMetadata beanQueryMetadata = findBeanQueryMetadata(method);
if (beanQueryMetadata != null) {
StalactiteQueryMethod queryMethod = new StalactiteQueryMethod(method, metadata, factory);
if (beanQueryMetadata.getBean() instanceof ExecutableEntityQuery) {
return new BeanRepositoryQuery<>(queryMethod, (ExecutableEntityQuery) beanQueryMetadata.getBean(), beanQueryMetadata.getCounterBean());
} else {
return new QueryRepositoryQuery<>(queryMethod, beanQueryMetadata.getBean(), beanQueryMetadata.getCounterBean(), dialect);
}
} else {
return null;
}
}
@VisibleForTesting
@javax.annotation.Nullable
BeanQueryMetadata findBeanQueryMetadata(Method method) {
Set<BeanQueryMetadata> queryMetadataSet = collectBeanQueryMetadata(method);
switch (queryMetadataSet.size()) {
case 0:
return null;
case 1:
return Iterables.first(queryMetadataSet);
default:
// Checking BeanQuery.repositoryClass
Set<BeanQueryMetadata> defaultBeans = queryMetadataSet.stream()
.filter(BeanQueryMetadata::isDefault).collect(Collectors.toSet());
Set<BeanQueryMetadata> dedicatedBeans = queryMetadataSet.stream()
.filter(metadata -> metadata.isFor((Class<? extends StalactiteRepository>) method.getDeclaringClass())).collect(Collectors.toSet());
if (dedicatedBeans.size() == 1) {
return Iterables.first(dedicatedBeans);
} else if (defaultBeans.size() == 1) {
return Iterables.first(defaultBeans);
}
throw new UnsupportedOperationException("Multiple beans found matching method " + Reflections.toString(method) + ": "
+ Iterables.collect(queryMetadataSet, metadata -> metadata.beanName, HashSet::new)
+ ", but none for repository type " + Reflections.toString(method.getDeclaringClass()) + ": "
+ Iterables.collect(queryMetadataSet, metadata -> Reflections.toString(metadata.queryAnnotation.repositoryClass()), ArrayList::new)
);
}
}
private Set<BeanQueryMetadata> collectBeanQueryMetadata(Method method) {
// short class dedicated to local algorithm for @Bean + @BeanQuery metadata storage
Map<String, Object> beansWithAnnotation = beanFactory.getBeansWithAnnotation(BeanQuery.class);
Set<BeanQueryMetadata> queryMetadataSet = beansWithAnnotation.entrySet().stream()
.map(entry -> {
BeanQuery beanQueryAnnotation = beanFactory.findAnnotationOnBean(entry.getKey(), BeanQuery.class);
Nullable<ExecutableProjectionQuery> counterBean = Nullable.nullable(beanQueryAnnotation).map(BeanQuery::counterBean)
.filter(Predicates.not(String::isEmpty))
.map(counterBeanName -> beanFactory.getBean(counterBeanName, ExecutableProjectionQuery.class));
return new BeanQueryMetadata(
entry.getKey(),
(ExecutableQuery) entry.getValue(),
counterBean.get(),
beanQueryAnnotation);
})
.filter(metadata ->
metadata.beanName.equals(method.getName())
|| Arrays.asList(metadata.queryAnnotation.name()).contains(method.getName())
|| metadata.queryAnnotation.method().equals(method.getName()))
.collect(Collectors.toSet());
return queryMetadataSet;
}
/**
* Internal class to store information about @{@link BeanQuery} while looking for it
* @author Guillaume Mary
*/
@VisibleForTesting
static class BeanQueryMetadata {
private final String beanName;
private final ExecutableQuery bean;
private final ExecutableProjectionQuery counterBean;
private final BeanQuery queryAnnotation;
private BeanQueryMetadata(String beanName, ExecutableQuery bean, ExecutableProjectionQuery counterBean, BeanQuery queryAnnotation) {
this.beanName = beanName;
this.bean = bean;
this.counterBean = counterBean;
this.queryAnnotation = queryAnnotation;
}
public ExecutableQuery getBean() {
return bean;
}
public ExecutableProjectionQuery getCounterBean() {
return counterBean;
}
boolean isDefault() {
return isFor(StalactiteRepository.class);
}
boolean isFor(Class<? extends StalactiteRepository> repositoryClass) {
return queryAnnotation.repositoryClass().equals(repositoryClass);
}
}
}